package com.flexfiltering.view.controls
{
import com.flexfiltering.view.controls.hierarchicalBrowserClasses.*;

import flash.events.Event;
import flash.utils.getTimer;

import mx.collections.ArrayCollection;
import mx.containers.HBox;
import mx.controls.Button;
import mx.controls.Label;
import mx.controls.TextInput;
import mx.core.EdgeMetrics;
import mx.core.UIComponent;
import mx.events.ListEvent;

[Event(name="change", type="flash.events.Event")]
public class HierarchyBrowser extends HBox
{
	// @------------------------------------------------------ STATIC PROPERTIES
	// @-------------------------------------------------------PUBLIC PROPERTIES
	
	//----------------------------------
	//  dataProvider
	//----------------------------------

	/**
	 *  @private
	 *  Change-flag for dataProvider property.
	 */
	private var dataProviderChanged:Boolean;

	/**
	 *  @private
	 *  Storage for the dataProvider property.
	 */
	private var _dataProvider:HierarchicalDataVO;

	[Bindable()]
	/**
	 *  
	 * 
	 * 	@default 
	 */ 
	public function get dataProvider() : Object
	{
	    return _dataProvider;
	}

	/**
	 *  @private
	 */
	public function set dataProvider ( val:Object ) : void
	{
		_dataProvider = val as HierarchicalDataVO;
		dataProviderChanged = true;
		invalidateProperties();
	}
	
	public var initListIndex:int = 0;
	
	public function initalizeList () : void
	{
		var items:Array;
		
		if (!_selectedItems[initListIndex].label.indexOf("Any") == 0)
		{
			selections.push([_selectedItems[initListIndex].data]);
			currentList = lists[initListIndex];
			updateLists();
			lists[initListIndex].selectedItems = [_selectedItems[initListIndex].data];
		}
		
		if ( initListIndex < maxDepth-1 )
		{
			initListIndex++;
			callLater(initalizeList);	
		}
	}
	
	private var _selectedItems:Array;

	public function set selectedItems ( vo:Array ) : void
	{
		_selectedItems = vo;
		
		everyListHasAll = true;
		
		for ( var i:int=0; i<vo.length; i++ )
		{
			var testString:String = vo[i].label;
			if (testString.indexOf("Any ") == -1)
			{
				everyListHasAll = false;
				break;					
			}
		}
		
		if (everyListHasAll && everyListAllLasttime)
		{
			return;
		}
		else
		{	
			selections=[];
			initListIndex = 0;
			if (vo.length > 0) 
			{
				initalizeList();
			}
		}
		
		if (everyListHasAll)
		{
			everyListAllLasttime = true;
		}
		else
		{
			everyListAllLasttime = false;
		}
	}
	
	public function set filterIndexString (val:String):void
	{
		
	}

	public var everyListHasAll:Boolean = true;
	public var collectionIndices:ArrayCollection;
	public var showLastList:Boolean = true;
	
	// @---------------------------------------------------- INTERNAL PROPERTIES
	
	protected var lists:Array; //<HierarchyLists>
	protected var collections:Array; //<Array>
	protected var currentList:HierarchicalNodeList;
	protected var everyListAllLasttime:Boolean = true;
	protected var selections:Array = [];
	
	protected var header:UIComponent;
	protected var footer:UIComponent;
	
	protected var filterInput:TextInput;
	protected var titleLabel:Label;
	protected var acceptButton:Button;
	protected var cancelButton:Button;
	
	protected function get maxDepth():Number
	{
		return lists.length;
	}
	
	// @-------------------------------------------------- OVERRIDDEN PROPERTIES
	
	override public function get viewMetrics():EdgeMetrics
	{
		var vm:EdgeMetrics = super.viewMetrics.clone();
		
		vm.top += getHeaderHeight();
		vm.bottom += getFooterHeight();
		
		return vm;
	}
	
	// @------------------------------------------------------------ CONSTRUCTOR
	
	public function HierarchyBrowser ()
	{
		super();
	}
	
	// @--------------------------------------------------------- PUBLIC METHODS
	
	//----------------------------------
	//  applyFilter
	//----------------------------------
	
	public function applyFilter (filterText:String) : void
	{
		this.filterInput.text = filterText;
	}
	
	//----------------------------------
	//  setListsToOriginalData
	//----------------------------------
	
	public function setListsToOriginalData() : void
	{
		if (everyListHasAll == true) return;
		var startX:int = getTimer();
		this.everyListAllLasttime = true;
		this.everyListHasAll = true;
		for (var i:Number=0; i < maxDepth; i++)
		{
			collections[i].source = _dataProvider.entries[i].toArray();
		}
	
		var totalTime:int = getTimer() - startX;
//		trace("total time to set lists to orignal data: " + totalTime);
	
	}
	
	// @------------------------------------------------------- INTERNAL METHODS
	
	//----------------------------------
	//  getHeaderHeight
	//----------------------------------
	
	protected function getHeaderHeight () : Number
	{
		return getStyle('headerHeight') || 28;	
	}
	
	//----------------------------------
	//  getFooterHeight
	//----------------------------------
	
	protected function getFooterHeight () : Number
	{
		return getStyle('headerHeight') || 28;	
	}
	
	//----------------------------------
	//  handleFilterChange
	//----------------------------------
	
	protected function handleFilterChange () : void
	{
		// TODO: test performance of filtering even when text is < 3 chars
		// we only want to filter if the user has typed more than 3 chars
		if (filterInput.length >= 2)
		{
			everyListHasAll = false;
			everyListAllLasttime = false;
			selections = [];
		}
		else
		{
			return;
		}
		
		var startFilter:int = getTimer();
		
		/*Filtering works as follows: 
		 *we search the lowercase index for the fillter string 
		 * for every match  we push it's path and label into  a number=maxDepth of new arrays
		 * we then update the source of these lists, displaying the filtered results
		*/
		
		//declare our new arrays and distinct indexes;
		var newArrays:Array = new Array();
		var distinctIndexes:Array = new  Array();
		for (var y:Number=0; y < maxDepth; y++)
		{
			newArrays.push(new Array());
			distinctIndexes.push("");
		}	
			
		var i:Number = 0;
		var pos:Number = 0;
		var chunk:String;
		var path:String;
		var count:Number = 0;
		var collectionIndex:String;
		var collectionDistinctIndex:String = "";
		collectionIndices = new ArrayCollection();
		
		//Start iterating through the index
		while ( i > -1 && i < _dataProvider.indexStringLowerCase.length)
		{
			count++;
			pos = _dataProvider.indexStringLowerCase.indexOf(filterInput.text.toLowerCase(),i);
			
			// once a match is found, parse out the node and each element of the path					
			if(pos > -1)
			{
				var endOfNodeIndex:int = _dataProvider.indexStringUpperCase.indexOf(">", pos);
				
				//the chunk is all the data between the last match and the last char of this node
				chunk = _dataProvider.indexStringUpperCase.substring(i, endOfNodeIndex);
					
				var startOfNodeIndex:int = chunk.lastIndexOf(">");
				path = chunk.substring(startOfNodeIndex+1);
	
				// advance the position past the end of this node
				i = endOfNodeIndex;
		
				//now add the node and it's path to our new arrays
				for (var p:Number=0; p < maxDepth; p++)
				{
					count++;
					var startIndex:Number = path.indexOf("[" + p +"]");
					if (startIndex != -1)
					{
						var node:String = path.substring(startIndex+ 3, path.indexOf(",", startIndex) );	
						if (p == (maxDepth -1))
						{
							//on the last node so substring to the end
							node =  path.substring(startIndex+ 3, path.length);	
						}
						
						//add the node to the current array if it's not already in there
						if ( distinctIndexes[p].indexOf("<" +node+ ">") == -1 )
						{
							distinctIndexes[p] += "<" +node+ ">";
							newArrays[p].push(node);
							
							//add the index of the original collection of this node to the collection indices
							collectionIndex = path.substring(2, path.lastIndexOf("|["));
							if ( collectionDistinctIndex.indexOf("<" + collectionIndex +">") == -1 )
							{
								collectionDistinctIndex+= "<" +collectionIndex + ">";
								collectionIndices.addItem(collectionIndex);
							}
						}
						
						
					}
				}
			}
			else
			{				break;
			}
		}
		
		var totalTime:int = getTimer() - startFilter;
//		trace("total time to find item " + totalTime + " and total count: " + count);
			
		//update our arrays
		for (var j:Number=0; j < maxDepth; j++)
		{
			collections[j].source = newArrays[j];
		}
		
	}
	
	//----------------------------------
	//  updateLists
	//----------------------------------
	
	protected function updateLists ( ) : void
	{	
		var i:Number = 0;
		var pos:Number = 0;
		var chunk:String;
		var path:String;
		var selectionsFoundFlag:Boolean = false;
		var collectionIndex:String;
		var collectionDistinctIndex:String = "";
		
		var currentSelections:Array;
			
		//Declare a new Array and new distinctString for each array to the right of the currently selected list
		var newArrays:Array = new Array();
		var distinctStrings:Array = new Array();
		for (var p:Number =0; p <=maxDepth; p++)
		{
			newArrays[p] = new Array();
			distinctStrings[p] = "";
		}
		
		// we need to check to see what is selected.  We'll start by looking
		// at the selections from the list the user selected and to the left 
		// (above from a depth perspective).  If that doesn't have any selections, 
		// we'll move up until we find some, or until we hit the end

		for (var k:Number= currentList.depth; k > -1; k--)
		{
			//we only need to find one selection because below we check matches up against the parent selections as well
			if (selectionsFoundFlag)
			{
				break;
			}
			currentSelections = selections[k];				
			
			for each ( var s:String in currentSelections )
			{	
				//we found at least one selection
				selectionsFoundFlag = true;
				
				// walk the string, and find instance of this selection-key
				i = 0;
				while ( i > -1 )
				{
					//we look in our index for all strings that have the selection as an ancestor
					pos = _dataProvider.indexStringUpperCase.indexOf("["+k+"]"+s,i);
						
					// now go through and update all arrays to the right of the selection with the match				
					if(pos > -1)
					{
						var endOfNodeIndex:int = _dataProvider.indexStringUpperCase.indexOf(">", pos);
						//the chunk is all the data between the last match and the last char of this node
						chunk = _dataProvider.indexStringUpperCase.substring(i, endOfNodeIndex);
						var startOfNodeIndex:int = chunk.lastIndexOf(">");
						path = chunk.substring(startOfNodeIndex+1);
							
						// advance the position past the end of this node
						i = endOfNodeIndex;
						
						//qualify the node futher.   If there are other selections further up, do they all match?
						//check depths up from the current selection
						if (!qualifyPath(path, k)) {
							continue;
						}
						
						//check that this path matches the filter string (if any)
						if ((filterInput.text.length >=3) && path.toLowerCase().indexOf(filterInput.text.toLowerCase()) == -1) {
							continue;
						}
						
						//go through and build out all new arrays to the right
						for (var t:Number = currentList.depth + 1; t<= maxDepth; t++) {
							//first make sure there is an entry in the path for this level, there won't be for empty subareas
							if ( path.indexOf("[" + t + "]") != -1) {
								var lastItemIndex:Number = path.indexOf("[" + (t+1) + "]");
								if (lastItemIndex  == -1) {
									lastItemIndex = path.length + 1;
								} 
								if ( path.indexOf("[" + (t +1) + "]") == -1 && t != maxDepth -1)  {
									lastItemIndex--;   //If there are no more columns then we take off the last comma
								}
								var item:String = path.substring(path.indexOf("[" + t + "]") + 3, lastItemIndex -1);
								//check that we haven't already added it
								if ( distinctStrings[t].indexOf("<" + item +">") == -1 )
								{
									distinctStrings[t] += "<" +item + ">";
									newArrays[t].push(item);
									
									collectionIndex = path.substring(2, path.lastIndexOf("|["));
									if ( collectionDistinctIndex.indexOf("<" + collectionIndex +">") == -1 )
									{
										collectionDistinctIndex+= "<" +collectionIndex + ">";
										collectionIndices.addItem(collectionIndex);
									}
								}
								
							
							}
						}
					}
					else
					{
						break;
					}
				}
			}
		}

		//go through and update the source of each collection	
		// if we never found any selections, we should just show the orginal
		// set of entries for this depth
		for (var j:Number = currentList.depth + 1; j< maxDepth; j++)
		{
			//if the user made no selections then everything is All, we can update to all the entries if there is no filter
			if (!selectionsFoundFlag)
			{
				if (filterInput.text.length < 3) {
					collections[j].source = _dataProvider.entries[j].toArray();
				} else {
					//if there is a filter and the user has chosen all, reapply the filter
					handleFilterChange();
				}
			}
			else
			{
				collections[j].source = newArrays[j];
			}	
		}
	}
	
	//----------------------------------
	//  qualifyPath
	//----------------------------------
	
	protected function qualifyPath ( path:String, depth:int ) : Boolean
	{
		var keep:Boolean = true;
		var selections:Array;
		
		// once we find a match, we can stop looking
		while ( depth > -1 && keep == true )
		{
			selections = this.selections[depth];
			for each ( var s:String in selections )
			{
				if ( path.indexOf("["+depth+"]"+s + ",") == -1 )
				{
					keep = false;
				}
			}
			
			depth--;
		}
		
		return keep;
	}
	// @----------------------------------------------------- OVERRIDDEN METHODS
	
	//----------------------------------
	//  createChildren
	//----------------------------------
	
	override protected function createChildren () : void
	{
		super.createChildren();
		
		if ( !header )
		{
			header = new UIComponent();
			header.tabChildren = true;
			rawChildren.addChild(header);
		}
		
		if ( !filterInput )
		{
			filterInput = new TextInput();
			filterInput.focusRect = false;
			filterInput.styleName = getStyle('filterInputStyleName');
			filterInput.addEventListener( Event.CHANGE,
										  filterInput_changeHandler );
			header.addChild(filterInput);
		}
		
		if ( !titleLabel )
		{
			titleLabel = new Label();
			titleLabel.styleName = getStyle('filterInputStyleName');
			
			header.addChild(titleLabel);
		}
		
		if ( !footer )
		{
			footer = new UIComponent();
			footer.tabChildren = true;
			rawChildren.addChild(footer);
		}
		
	}
	
	private function dataTipHandler(obj:Object) : String 
	{
		return obj.toString();
	}
	
	
	//----------------------------------
	//  commitProperties
	//----------------------------------
	
	override protected function commitProperties():void
	{
		super.commitProperties();
		
		if ( dataProviderChanged )
		{
			var list:HierarchicalNodeList;
			
			if ( lists && lists.length > 0 )
			{
				for each ( list in lists )
				{
					removeChild(list);
				}
			}
			
			lists = [];
			collections = []
			
			for ( var i:int=0; i<_dataProvider.entries.length; i++ )
			{
				collections[i] = new ArrayCollection(_dataProvider.entries[i].source);
				list = new HierarchicalNodeList();
				list.depth = i;
				list.showDataTips = false;
				list.dataTipFunction = dataTipHandler;
				list.dataProvider = collections[i];
				list.label = _dataProvider.labels[i];
				list.browser = this;
				list.percentWidth = 100;
				list.percentHeight = 100;
				list.includeInLayout = (i == _dataProvider.entries.length - 1) && !showLastList ? false : true;
				list.visible = (i == _dataProvider.entries.length - 1) && !showLastList ? false : true;
				list.addEventListener( ListEvent.CHANGE,
									   list_changeHandler );
				lists.push(list);
				
				addChild(list);
			}
			
			invalidateDisplayList();
			
			dataProviderChanged = false;
		}
	}
	
	override protected function layoutChrome (w:Number, h:Number):void
	{
		super.layoutChrome(w,h);
		
		header.setActualSize( w-borderMetrics.left-borderMetrics.right,
							  getHeaderHeight() );
							 
		header.x = borderMetrics.left;
		header.y = borderMetrics.right;
		
		titleLabel.setActualSize( titleLabel.getExplicitOrMeasuredWidth(),
								  titleLabel.getExplicitOrMeasuredHeight() );
								  
		titleLabel.x = 0;
		titleLabel.y = (header.height - titleLabel.height)/2;
		
		filterInput.setActualSize( filterInput.getExplicitOrMeasuredWidth(),
								   filterInput.getExplicitOrMeasuredHeight() );
								   
		filterInput.x = header.width-filterInput.width;
		filterInput.y = (header.height - filterInput.height)/2;
		
		footer.setActualSize( w-borderMetrics.left-borderMetrics.right,
							  getFooterHeight() );
		
		footer.x = borderMetrics.left;
		footer.y = h - footer.height - borderMetrics.bottom;
		
	}
	
	// @--------------------------------------------------------- EVENT HANDLERS
	
	
	//----------------------------------
	//  list_changeHandler
	//----------------------------------
	
	protected function list_changeHandler ( event:ListEvent ) : void
	{
		collectionIndices = new ArrayCollection();
		currentList = event.target as HierarchicalNodeList ;
		
		var allSelected:Boolean;
		var si:Array = [];

		for each ( var s:String in currentList.selectedItems )
		{
			if ( s.indexOf("All ") != -1)
			{
				allSelected = true;
				everyListHasAll = true;
				break;
			}
			else
			{
				everyListHasAll = false;
				everyListAllLasttime = false;
				si.push(s);	
			}
		}
		
		if (allSelected)
		{
			selections[currentList.depth] = null;
		}
		else
		{
			selections[currentList.depth] = si;
		}
		
		updateLists();
		dispatchEvent(new Event("change"));
	}
	
	//----------------------------------
	//  filterInput_changeHandler
	//----------------------------------
	
	protected function filterInput_changeHandler ( event:Event ) : void
	{
		if ( filterInput.length > 2 )
		{
			handleFilterChange();
		}
		else
		{
			setListsToOriginalData();
		}
		dispatchEvent(new Event(Event.CHANGE)); 
	}
	
	// @---------------------------------------------- OVERRIDDEN EVENT HANDLERS
	

} // class
} // package